In [3]:
import networkx as nx
from datetime import datetime
import matplotlib.pyplot as plt
import numpy as np
%matplotlib inline
As mentioned earlier, networks, also known as graphs, are comprised of individual entities and their representatives. The technical term for these are nodes and edges, and when we draw them we typically use circles (nodes) and lines (edges).
In this notebook, we will work with a synthetic (i.e. simulated) social network, in which nodes are individual people, and edges represent their relationships. If two nodes have an edge between them, then those two individauls know one another.
In the networkx
implementation, graph objects store their data in dictionaries.
Nodes are part of the attribute Graph.node
, which is a dictionary where the key is the node ID and the values are a dictionary of attributes.
Edges are part of the attribute Graph.edge
, which is a nested dictionary. Data are accessed as such: G.edge[node1][node2]['attr_name']
.
Because of the dictionary implementation of the graph, any hashable object can be a node. This means strings and tuples, but not lists and sets.
With this synthetic social network, we will attempt to answer the following basic questions using the NetworkX API:
First off, let's load up the synthetic social network. This will show you through some of the basics of NetworkX.
For those who are interested, I simply created an Erdõs-Rényi graph with n=30
and p=0.1
. I used randomized functions that I wrote to generate attributes and append them to each node and edge. I then pickled the graph to disk.
In [4]:
G = nx.read_gpickle('Synthetic Social Network.pkl') #If you are Python 2.7, read in Synthetic Social Network 27.pkl
nx.draw(G)
In [5]:
# Who are represented in the network?
G.nodes(data=True)
Out[5]:
Exercise: Can you write a single line of code that returns the number of individuals represented?
In [6]:
len(G.nodes())
Out[6]:
In [7]:
# Who is connected to who in the network?
G.edges(data=True)
Out[7]:
In [8]:
len(G.edges())
Out[8]:
Since this is a social network of people, there'll be attributes for each individual, such as age, and sex. We can grab that data off from the attributes that are stored with each node.
In [9]:
# Let's get a list of nodes with their attributes.
G.nodes(data=True)
# NetworkX will return a list of tuples in the form (node_id, attribute_dictionary)
Out[9]:
In [10]:
from collections import Counter
Counter([d['sex'] for n, d in G.nodes(data=True)])
Out[10]:
Edges can also store attributes in their attribute dictionary.
In [11]:
G.edges(data=True)
Out[11]:
In this synthetic social network, I have stored the date as a datetime object. Datetime objects have attributes, namely .year
, .month
, .day
.
In [12]:
# Answer
dates = [d['date'] for _, _, d in G.edges(data=True)]
mindate = min(dates)
maxdate = max(dates)
print(mindate, maxdate)
We found out that there are two individuals that we left out of the network, individual no. 31 and 32. They are one male (31) and one female (32), their ages are 22 and 24 respectively, they knew each other on 2010-01-09, and together, they both knew individual 7, on 2009-12-11. Use the functions G.add_node()
and G.add_edge()
to introduce this data into the network.
If you need more help, check out https://networkx.github.io/documentation/latest/tutorial/tutorial.html
In [13]:
# Answer
G.add_node(31, age=22, sex='Male')
G.add_node(32, age=24, sex='Female')
G.add_edge(31, 32, date=datetime(2010,1,9))
G.add_edge(31, 7, date=datetime(2009,12,11))
G.add_edge(32, 7, date=datetime(2009,12,11))
G.node[31]
Out[13]:
While we're on the matter of graph construction, let's take a look at our tutorial class. On your sheet of paper, you should have a list of names - these are people for which you knew their name prior to coming to class.
As we iterate over the class, I would like you to holler out your name, your nationality, and in a very slow fashion, the names of the people who you knew in the class.
In [14]:
ptG = nx.DiGraph() #ptG stands for PyCon Tutorial Graph.
# Add in nodes and edges
ptG.add_node('Eric', nationality='Canada')
ptG.add_node('Paul', nationality='Canada') # (my own TextExpander shortcut is ;addnode)
ptG.add_node('Max', nationality='US')
ptG.add_node('Martin', nationality='Other')
ptG.add_node('Jim', nationality='US')
ptG.add_node('Lucas', nationality='US')
ptG.add_node('Thomas', nationality='US')
ptG.add_node('Brad', nationality='US')
ptG.add_node('Troy', nationality='Canada')
ptG.add_node('Cory', nationality='Canada')
ptG.add_node('Gokhan', nationality='US')
ptG.add_node('Riley', nationality='US')
ptG.add_node('Steve', nationality='US')
ptG.add_node('Ryan', nationality='US')
ptG.add_node('Andrew', nationality='US')
ptG.add_node('Ronan', nationality='Other')
ptG.add_node('Cody', nationality='Canada')
ptG.add_node('Jon', nationality='US')
ptG.add_node('Eric2', nationality='US')
ptG.add_node('William', nationality='US')
ptG.add_node('Tom', nationality='Other')
ptG.add_node('Chris', nationality='US')
ptG.add_node('Stu', nationality='US')
ptG.add_node('Zach', nationality='US')
ptG.add_node('Clint', nationality='Canada')
ptG.add_node('Aaron', nationality='US')
ptG.add_node('Vishal', nationality='US')
ptG.add_node('Federico', nationality='Other')
ptG.add_edge('Vishal', 'Aaron')
ptG.add_edge('Vishal', 'Eric')
ptG.add_edge('Aaron', 'Vishal')
ptG.add_edge('Aaron', 'Eric')
ptG.add_edge('Clint', 'Zach')
ptG.add_edge('Clint', 'Eric')
ptG.add_edge('Zach', 'Clint')
ptG.add_edge('Zach', 'Riley')
ptG.add_edge('Zach', 'Stu')
ptG.add_edge('Stu', 'Zach')
ptG.add_edge('Stu', 'Eric')
ptG.add_edge('Stu', 'Chris')
ptG.add_edge('Chris', 'Stu')
ptG.add_edge('Chris', 'Eric')
ptG.add_edge('Tom', 'Tom')
ptG.add_edge('William', 'Jon')
ptG.add_edge('William', 'Eric2')
ptG.add_edge('William', 'Eric')
ptG.add_edge('Eric2', 'William')
ptG.add_edge('Eric2', 'Jon')
ptG.add_edge('Jon', 'Eric2')
ptG.add_edge('Jon', 'William')
ptG.add_edge('Jon', 'Eric')
ptG.add_edge('Cody', 'Eric')
ptG.add_edge('Cody', 'Ronan')
ptG.add_edge('Ronan', 'Eric')
ptG.add_edge('Ronan', 'Cody')
ptG.add_edge('Andrew', 'Eric')
ptG.add_edge('Andrew', 'Ryan')
ptG.add_edge('Ryan', 'Eric')
ptG.add_edge('Ryan', 'Andrew')
ptG.add_edge('Steve', 'Eric')
ptG.add_edge('Riley', 'Zach')
ptG.add_edge('Paul', 'Paul') # (my own TextExpander shortcut is ;addedge)
ptG.add_edge('Martin', 'Max')
ptG.add_edge('Max', 'Paul')
ptG.add_edge('Martin', 'Eric')
ptG.add_edge('Martin', 'Max')
ptG.add_edge('Jim', 'Federico')
ptG.add_edge('Lucas', 'Thomas')
ptG.add_edge('Brad', 'Eric')
ptG.add_edge('Thomas', 'Lucas')
ptG.add_edge('Troy', 'Cory')
ptG.add_edge('Troy', 'Eric')
ptG.add_edge('Cory', 'Troy')
ptG.add_edge('Gokhan', 'Max')
In [16]:
# We are now going to draw the network using a hive plot, grouping the nodes by the top two nationality groups, and 'others'
# for the third group.
nodes = dict()
nodes['Canada'] = [n for n, d in ptG.nodes(data=True) if d['nationality'] == 'Canada'] #list comprehension here
nodes['US'] = [n for n, d in ptG.nodes(data=True) if d['nationality'] == 'US'] #list comprehension here
nodes['Other'] = [n for n, d in ptG.nodes(data=True) if d['nationality'] == 'Other'] #list comprehension here
edges = dict()
edges['group1'] = [(n1, n2, d) for n1, n2, d in ptG.edges(data=True)] #list comprehension here
nodes_cmap = dict()
nodes_cmap['Canada'] = 'blue'
nodes_cmap['US'] = 'green'
nodes_cmap['Other'] = 'black'
edges_cmap = dict()
edges_cmap['group1'] = 'black'
from hiveplot import HivePlot
h = HivePlot(nodes, edges, nodes_cmap, edges_cmap)
h.set_minor_angle(np.pi / 12) #optional
h.draw()
A similar pattern can be used for edges:
[n2 for n1, n2, d in G.edges(data=True)]
or
[n2 for _, n2, d in G.edges(data=True)]
If the graph you are constructing is a directed graph, with a "source" and "sink" available, then I would recommend the following pattern:
[(sc, sk) for sc, sk, d in G.edges(data=True)]
or
[d['attr'] for sc, sk, d in G.edges(data=True)]
In [17]:
nx.draw(G)
If the network is small enough to visualize, and the node labels are small enough to fit in a circle, then you can use the with_labels=True
argument.
In [18]:
nx.draw(G, with_labels=True)
However, note that if the number of nodes in the graph gets really large, node-link diagrams can begin to look like massive hairballs. This is undesirable for graph visualization.
Instead, we can use a matrix to represent them. The nodes are on the x- and y- axes, and a filled square represent an edge between the nodes. This is done by using the nx.to_numpy_matrix(G)
function.
We then use matplotlib
's pcolor(numpy_array)
function to plot. Because pcolor
cannot take in numpy matrices, we will cast the matrix as an array of arrays, and then get pcolor
to plot it.
In [19]:
matrix = nx.to_numpy_matrix(G)
plt.pcolor(np.array(matrix))
plt.axes().set_aspect('equal') # set aspect ratio equal to get a square visualization
plt.xlim(min(G.nodes()), max(G.nodes())) # set x and y limits to the number of nodes present.
plt.ylim(min(G.nodes()), max(G.nodes()))
plt.title('Adjacency Matrix')
plt.show()
Let's try another visualization, the Circos plot. We can order the nodes in the Circos plot according to the node ID, but any other ordering is possible as well. Edges are drawn between two nodes.
Credit goes to Justin Zabilansky (MIT) for the implementation.
In [20]:
from circos import CircosPlot
fig = plt.figure(figsize=(6,6))
ax = fig.add_subplot(111)
nodes = sorted(G.nodes())
edges = G.edges()
c = CircosPlot(nodes, edges, radius=10, ax=ax)
c.draw()
It's pretty obvious in this visualization that there are nodes, such as node 5 and 18, that are not connected to any other node via an edge. There are other nodes, like node number 19, which is highly connected to other nodes.
Finally, let's try hive plots for the network. Two groups (male and female), and then edges drawn between them.
In [21]:
nodes = dict()
nodes['male'] = [n for n,d in G.nodes(data=True) if d['sex'] == 'Male']
nodes['female'] = [n for n,d in G.nodes(data=True) if d['sex'] == 'Female']
edges = dict()
edges['group1'] = G.edges(data=True)
nodes_cmap = dict()
nodes_cmap['male'] = 'blue'
nodes_cmap['female'] = 'red'
edges_cmap = dict()
edges_cmap['group1'] = 'black'
h = HivePlot(nodes, edges, nodes_cmap, edges_cmap)
h.draw()
In [ ]:
In [ ]: